iT邦幫忙

2023 iThome 鐵人賽

DAY 5
0

昨天看過了 route() 的實作內

val selector = HttpMethodRouteSelector(method)

裡面的邏輯。

今天,我們來看看

return createRouteFromPath(path).createChild(selector).apply(build)

這段程式,是怎麼處理我們輸入的路徑的。

首先看 createRouteFromPath() 的實作

public fun RoutingBuilder.createRouteFromPath(path: String): RoutingBuilder {
    val parts = RoutingPath.parse(path).parts
    var current: RoutingBuilder = this
    for (index in parts.indices) {
        val (value, kind) = parts[index]
        val selector = when (kind) {
            RoutingPathSegmentKind.Parameter -> PathSegmentSelectorBuilder.parseParameter(value)
            RoutingPathSegmentKind.Constant -> PathSegmentSelectorBuilder.parseConstant(value)
        }
        // there may already be entry with same selector, so join them
        current = current.createChild(selector)
    }
    if (path.endsWith("/")) {
        current = current.createChild(TrailingSlashRouteSelector)
    }
    return current
}

我們開始看 val parts = RoutingPath.parse(path).parts

RoutingPath 定義如下

public class RoutingPath private constructor(public val parts: List<RoutingPathSegment>)

RoutingPath.parse() 的實作則如下

public fun parse(path: String): RoutingPath {
	if (path == "/") return root
	val segments = path.splitToSequence("/").filter { it.isNotEmpty() }.map { segment ->
		when {
			segment.contains('{') && segment.contains('}') -> RoutingPathSegment(
				segment,
				RoutingPathSegmentKind.Parameter
			)
			else -> RoutingPathSegment(segment.decodeURLPart(), RoutingPathSegmentKind.Constant)
		}
	}

	return RoutingPath(segments.toList())
}

我們一段一段的看這段程式碼

if (path == "/") return root

這段很單純:如果傳入的 path/,不需要做任何特殊處理,回傳

public val root: RoutingPath = RoutingPath(listOf())

即可。

如果是比較複雜的路徑,那就走到下一段處理:

val segments = path.splitToSequence("/").filter { it.isNotEmpty() }.map {

}

根據斜線拆分之後,過濾掉空的元素,然後利用 map() 做區分

when {
	segment.contains('{') && segment.contains('}') -> RoutingPathSegment(
		segment,
		RoutingPathSegmentKind.Parameter
	)
	else -> RoutingPathSegment(segment.decodeURLPart(), RoutingPathSegmentKind.Constant)
}

根據命名,看起來是將路徑區分成 ParameterConstant

要確認這個假設,我們進去看 RoutingPathSegmentRoutingPathSegmentKind 的實作

/**
 * A single routing path segment.
 * @property value - segment text value
 * @property kind - segment kind (constant or parameter)
 */
public data class RoutingPathSegment(val value: String, val kind: RoutingPathSegmentKind)

/**
 * Possible routing path segment kinds.
 */
public enum class RoutingPathSegmentKind {
    /**
     * A constant path segment.
     */
    Constant,

    /**
     * A parameter path segment (a wildcard, a named parameter, or both).
     */
    Parameter
}

僅僅用了一個 data class 和一個 enum class ,就成功地定義了路徑所需要的元素種類!

最後回傳 RoutingPath(segments.toList()),所以前面的

val parts = RoutingPath.parse(path).parts

就可以拿到路徑拆分之後的 List<RoutingPathSegment> 了。

取得之後,就是針對這個 List 內每個元素進行操作

for (index in parts.indices) {
	val (value, kind) = parts[index]
	val selector = when (kind) {
		RoutingPathSegmentKind.Parameter -> PathSegmentSelectorBuilder.parseParameter(value)
		RoutingPathSegmentKind.Constant -> PathSegmentSelectorBuilder.parseConstant(value)
	}
	// there may already be entry with same selector, so join them
	current = current.createChild(selector)
}

上面這段內,

val (value, kind) = parts[index]

利用了 Kotlin 程式語言的 Destructuring declarations,來簡化取得 RoutingPathSegment 的元素程式碼。

之後根據對應的 RoutingPathSegmentKind 選擇 parse 方式,

PathSegmentSelectorBuilder.parseParameter 實作如下

public fun parseParameter(value: String): RouteSelector {
	val prefixIndex = value.indexOf('{')
	val suffixIndex = value.lastIndexOf('}')

	val prefix = if (prefixIndex == 0) null else value.substring(0, prefixIndex)
	val suffix = if (suffixIndex == value.length - 1) null else value.substring(suffixIndex + 1)

	val signature = value.substring(prefixIndex + 1, suffixIndex)
	return when {
		signature.endsWith("?") -> PathSegmentOptionalParameterRouteSelector(signature.dropLast(1), prefix, suffix)
		signature.endsWith("...") -> {
			if (suffix != null && suffix.isNotEmpty()) {
				throw IllegalArgumentException("Suffix after tailcard is not supported")
			}
			PathSegmentTailcardRouteSelector(signature.dropLast(3), prefix ?: "")
		}

		else -> PathSegmentParameterRouteSelector(signature, prefix, suffix)
	}
}

這段邏輯是很純粹的字串處理,狀態有一點多,但是邏輯相對單純。

PathSegmentSelectorBuilder.parseConstant 實作起來就要考慮一些狀況

public fun parseConstant(value: String): RouteSelector = when (value) {
	"*" -> PathSegmentWildcardRouteSelector
	else -> PathSegmentConstantRouteSelector(value)
}

根據輸入的元素,必須拆分成 PathSegmentWildcardRouteSelectorPathSegmentConstantRouteSelector

public object PathSegmentWildcardRouteSelector : RouteSelector() {
    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
        if (segmentIndex < context.segments.size && context.segments[segmentIndex].isNotEmpty()) {
            return RouteSelectorEvaluation.WildcardPath
        }
        return RouteSelectorEvaluation.FailedPath
    }

    override fun toString(): String = "*"
}
/**
 * Evaluates a route against a constant path segment.
 * @param value is a value of the path segment
 */
public data class PathSegmentConstantRouteSelector(
    val value: String
) : RouteSelector() {

    @Suppress("UNUSED_PARAMETER")
    @Deprecated(
        "hasTrailingSlash is not used anymore. This is going to be removed",
        level = DeprecationLevel.ERROR,
        replaceWith = ReplaceWith("PathSegmentConstantRouteSelector(value)")
    )
    public constructor(value: String, hasTrailingSlash: Boolean) : this(value)

    override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = when {
        segmentIndex < context.segments.size && context.segments[segmentIndex] == value ->
            RouteSelectorEvaluation.ConstantPath

        else -> RouteSelectorEvaluation.FailedPath
    }

    override fun toString(): String = value
}

到這邊,我們終於追完了 createRouteFromPath(path) 的邏輯,

知道最終怎麼將一段路徑拆分成 RoutingBuilder 物件。

接著就是對這個物件做 createChild()

/**
 * Creates a child node in this node with a given [selector] or returns an existing one with the same selector.
 */
public fun createChild(selector: RouteSelector): Route {
	val existingEntry = childList.firstOrNull { it.selector == selector }
	if (existingEntry == null) {
		val entry = Route(this, selector, developmentMode, environment)
		childList.add(entry)
		return entry
	}
	return existingEntry
}

加入了新的 selector 之後,接著就是利用 apply(build) 對回傳的 Route 物件加上邏輯了。

到這邊,我們總算將程式碼內

routing {
	get("/") {

	}
}

這兩個函數的實作看過了一遍。沒想到要宣告一個路徑,框架背後竟然做了這麼多的事情呢!

明天我們就來看看回傳的設定 call.respondText("Hello World!") 這一段程式內,框架又幫我們做了多少的事情吧!


上一篇
Day 04:建立路徑,來看看 get() 函數
下一篇
Day 06:處理回傳的內容,call.respondText() 前段
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言